<?php

namespace app\minichakra\controller\v1;

use app\common\cachemodel\chakra\ChakraAppUser;
use app\common\cachemodel\chakra\ChakraLoginLog;
use app\common\cachemodel\chakra\ChakraMiniappUser;
use app\common\lib\Aes;
use app\common\lib\exception\ApiException;
use app\common\lib\IAuth;
use app\common\lib\mini\alipay\AmpHelper;
use app\common\lib\Util;
use app\common\model\AppTelLogin;
use app\minichakra\controller\ChakraAuth;
use think\Cache;
use think\Exception;
use think\Log;
use think\response\Json;
use XznsTools\curl\Curl;

/**
 * Class Login
 */
class Login extends ChakraAuth
{
    private const LOG_PRE = '脉轮 小程序Login Controller 异常：';

    private const WEAPP_SESSION_KEY = 'chakra_miniapp:session_key:weapp';
    private const TT_SESSION_KEY = 'chakra_miniapp:session_key:tt';

    public function getCode()
    {
        $tel = input('tel/s', '');// 账号注销时，手机号为空
        $tel = (new Aes())->decryptWithOpenssl($tel, 'chakraapp');
        $areaCode = input('area_code/d', 86);
        // 检查手机号是否正确
        if (!$tel || !Util::telRuleCheck($tel, $areaCode))
        {
            return show(config('code.error'), '手机号错误');
        }
        /**
         * 过滤非法获取验证码请求，直接返回发送成功
         */
        if (Util::isIllegalRequest(true))
        {
            return show(config('code.success'), '发送成功');
        }
        $telStr = 'chakra:isSend:' . $areaCode . '-' . $tel;
        $isSendCode = Cache::get($telStr, false);
        if ($isSendCode)
        {
            return show(config('code.error'), '操作频繁，请稍后再试～');
        }
        $time = time();// 当前时间  有效时间暂定为十分钟 600s
        $expire = 600;
        $ip = $this->request->ip();
        $appTelLoginModel = new AppTelLogin();
        try
        {
            $data = $appTelLoginModel->where(['tel' => $tel, 'expire_time' => ['>', $time]])->findOrFail()->toArray();
        }
        catch (\Exception $e)
        {
            $data = [];
        }
        // 十分钟内无已有验证码
        if (empty($data))
        {
            $code = random_int(100000, 999999);// 六位验证码
            $expire_time = $time + $expire;
            try
            {
                $appTelLoginModel->insertGetId([
                    'tel' => "$tel",
                    'create_time' => $time,
                    'expire_time' => $expire_time,
                    'code' => $code,
                ]);
            }
            catch (\Exception $e)
            {
                Log::write('脉轮 获取手机验证码失败：' . $e->getMessage() . PHP_EOL . '失败行数：' . $e->getLine() . PHP_EOL . '错误文件：【' . $e->getFile() . '】');
                return show(config('code.error'), '发送失败');
            }

        } else
        {
            $code = $data['code'];
        }
        // 登录或绑定
        $result = Util::sendHySms([$tel], '', 154793, ['valid_code' => $code], '【星座女神脉轮】');
        if (!$result)
        {
            return show(config('code.error'), '发送失败');
        }
        // 缓存 验证码 10分钟
        Cache::set('chakra:tel_code:' . $tel, $code, 600);

        Cache::set($telStr, true, 50);
        // 记录短信日志
        model('log')->save([
            'tel' => "$tel",
            'ip' => $ip,
            'create_time' => $time,
            'type' => 1,
        ]);

        return show(config('code.success'), '发送成功');
    }

    /**
     * @title 手机验证码登录
     * @method POST
     * @return Json
     * @throws ApiException
     */
    public function mobile()
    {
        $tel = input('tel/s', '');
        $tel = (new Aes())->decryptWithOpenssl($tel, 'chakraapp');
        $code = input('code/s', '');
        $areaCode = input('area_code/d', 86);
        $time = $this->request->time();
        $ip = $this->request->ip();
        $appConfig = config('chakraapp');
        // 手机号不可注册
        $user = $this->CheckUserTel($tel, $areaCode);
        // 短信验证码 或 密码是否存在
        if (empty($code))
        {
            return show(config('code.error'), '手机短信验证码');
        }
        $isRefresh = true;
        // 短信登录时，检测验证码是否正确
        if (!empty($code))
        {
            if (!in_array($tel, $appConfig['mobile']) || ($areaCode === 86 && $code != substr($appConfig['code'] * (substr($tel, 3, 6)), 0, 6)))
            {
                //苹果和安卓应用商店审核账号，固定验证码为666666
                if (($tel == '13045794520' && $code == '666666') || ($tel == '13261877781' && $code == '666666'))
                {
                } else
                {
                    //  validate 严格校验
                    $mobileCode = Cache::get('chakra:tel_code:' . $tel);
                    if ($code != $mobileCode)
                    {
                        return show(config('code.error'), '短信验证码错误');
                    }
                }
            } else
            {
                $isRefresh = false;
            }
        }
        // 生成token  和 token到期时间
        $token = IAuth::setAppLoginToken($tel);
        $data = [
            'token' => $token,
            'token_expire' => strtotime("+" . $appConfig['login_time_out_day'] . " days"),
        ];
        $UserModel = new ChakraAppUser();
        //判断用户是否绑定准了号
        if ($user)
        {
            $UserModel->check_uen($user);
        }
        $nickname = preg_replace('/(1[3456789]{1}[0-9])[0-9]{4}([0-9]{4})/i', '$1****$2', $tel);
        // 该用户是否正常  forbidden==1 禁止登录
        if ($user && $user['forbidden'] == 0)
        {   // 已过期重新生成access_token
            $resUpdate = [];
            if ($isRefresh || $time > $user['token_expire'] || !in_array($tel, $appConfig['mobile']))
            {
                $resUpdate = $data;
            } else
            {
                $token = $user['token'] ?? '';
            }
            if (!$user['nickname'])
            {
                $resUpdate['nickname'] = $nickname;
            }
            if ($resUpdate)
            {
                $UserModel->updateUserinfo(['id' => $user['id']], $resUpdate);
            }
        } elseif ($user && $user['forbidden'] == 1)
        {
            return show(config('code.error'), '该账号禁止登录');
        } //手机号不可注册
        elseif (!$user && !empty($code) && !empty($tel))
        {
            // 第一次登录 注册数据
            $user['uen'] = substr(md5($this->request->time(true) . mt_rand(0, 1000000) . uniqid()), 0, 12);
            $user['nickname'] = $nickname;
            $user['reg_time'] = $time;
            $user['reg_ip'] = $ip;
            $user['last_login'] = $time;
            $user['last_ip'] = $ip;
            $user['last_active'] = $time;
            $user['status'] = 1;
            $user['sex'] = 1;
            $user['tel'] = $areaCode === 86 ? $tel : $areaCode . '-' . $tel; // 国内手机号不拼接国际手机区号
            $user['is_first_login'] = 1;
            $user['uc_id'] = md5($tel . $this->request->time(true));
            $user['token'] = $data['token'];
            $user['token_expire'] = $data['token_expire'];
            $user['has_self_recode'] = 0;
            $user['channel'] = $this->headers['channel'] ?? 0;
            $user['app_type'] = $this->headers['app-type'] ?? '';
            try
            {
                $user['id'] = $UserModel->insertGetId($user);
            }
            catch (\Exception $e)
            {
                return show(config('code.error'), '注册失败,请稍后重试');
            }
        }

        if ($user)
        {
            $id = (int)($user['id']);
            $result = [
                'access_token' => (new Aes())->encryptWithOpenssl($token . "||" . $user['id'], 'chakraapp'),
                'nickname' => $nickname,
                'avatar' => '/mini/chakra/imgs/avatar/default_avatar.png',
            ];
            // 记录登陆日志
            (new ChakraLoginLog())->insertGetId([
                'uid' => $id,
                'login_time' => $time,
                'login_type' => 4,
                'login_ip' => $ip,
            ]);
            return show(config('code.success'), '登录成功', $result);
        }
        return show(config('code.error'), '登录失败');
    }

    public function otherLogin()
    {
        $code = input('code/s');

        $env_type = $this->request->header('app-type');

        if (!in_array($env_type, ['WEAPP', 'ALIPAY', 'TT']))
        {
            return show(config('code.error'), '服务异常，不支持的设备～');
        }
        //由于支付宝小程序的登录比较特殊，需要单做
        if ($env_type === 'ALIPAY')
        {
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'avatar' => input('avatar/s'),
                'nickname' => input('nickname/s'),
                'sex' => input('sex/d'),
                'city' => input('city/s'),
                'province' => input('province/s'),
                'country' => input('country_code/s'),
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('alipay')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
            //获取user_id，返回结果参考：https://docs.open.alipay.com/api_9/alipay.system.oauth.token
            $tokenData = AmpHelper::getAmpToken($code);
            if (!isset($tokenData['user_id']))
            {
                //记录支付宝通过code获取用户user_id失败的错误日志
                Log::write(self::LOG_PRE . json_encode($tokenData), 'error', true);
                return show(config('code.error'), '获取用户支付宝id失败!');
            }
            return $this->userLogin(array_merge($params, $tokenData));
        }
        $encrypted_data = input('encrypted_data/s');
        $iv = input('iv/s');
        //TT：字节跳动小程序；
        $tt_raw_data = input('tt_raw_data/s');
        $tt_signature = input('tt_signature/s');
        $mini_item_conf = config('chakraapp')[$env_type];

        if ($env_type === 'TT')
        {   // 抖音小程序
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'iv' => $iv,
                'encrypted_data' => $encrypted_data,
                'tt_raw_data' => $tt_raw_data,
                'tt_signature' => $tt_signature,
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('tt')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
            $codeData = $this->getSessionKey($code, $env_type, $mini_item_conf);
            $session_key = $codeData['session_key'];

            if (empty($session_key))
            {
                return show(config('code.error'), '获取session_key异常！');
            }

            //先校验数据是否合法
            if ($tt_signature !== sha1($tt_raw_data . $session_key))
            {
                return false;
            }
            $data = $this->ttDecrypt($encrypted_data, $iv, $session_key);
            if ($data)
            {
                return $this->userLogin(array_merge($codeData, $params, $data), $session_key);
            }
        } else
        {   // 微信小程序
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'iv' => $iv,
                'encrypted_data' => $encrypted_data,
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('others')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }

            $session_key = $this->getSessionKey($code, $env_type, $mini_item_conf);
            if (empty($session_key))
            {
                return show(config('code.error'), '获取session_key异常！');
            }

            $data = $this->wechatOrQQDecrypt($mini_item_conf['app_id'], $session_key, $encrypted_data, $iv);
            if ($data)
            {
                return $this->userLogin(array_merge($params, $data), $session_key);
            }
        }

        return show(config('code.error'), '登录失败！');
    }

    /**
     * 用户登录统一处理
     * @param $data
     * @param string $sessionKey
     * @return Json|null
     * @throws ApiException
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    private function userLogin($data = [], string $sessionKey = ''): ?Json
    {
        $third_id = '';
        $miniType = $data['mini_type'];
        $nickname = '';
        $avatar = '';
        $sex = '';
        $unionid = '';
        $openid = '';
        $city = '';
        $province = '';
        $country = '';
        $where = ['app_type' => 1, 'mini_type' => $miniType];
        if (isset($data['app_type']))
        {
            $where['app_type'] = $data['app_type'];
        }
        switch ($miniType)
        {
            case 'ALIPAY':
                $third_id = $data['user_id'];
                $nickname = $data['nickname'];
                $avatar = $data['avatar'];
                $sex = $data['sex'];
                $unionid = '';
                $openid = $third_id;
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                $where['openid'] = $data['user_id'];
                $loginType = 11;
                break;
            case 'WEAPP':
                $third_id = $data['unionId'];
                $nickname = $data['nickName'];
                $avatar = $data['avatarUrl'];
                $sex = $data['gender'];
                $unionid = $data['unionId'];
                $openid = $data['openId'];
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                $where['unionid'] = $data['unionId'];
                $loginType = 10;
                break;
            case 'TT':
                $third_id = $data['openid'];
                $nickname = $data['nickName'];
                $avatar = $data['avatarUrl'];
                $sex = $data['gender'];
                $unionid = $data['unionid'];
                $openid = $third_id;
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                $where['openid'] = $data['openid'];
                $loginType = 12;
                break;
            default:
                return show(config('code.error'), '服务异常，不支持的设备～');
        }
        $UserModel = new ChakraAppUser();
        try
        {
            $tokenData = $this->getUserTokenInfo($third_id);
            $MiniAppUser = new ChakraMiniappUser();
            $time = $this->request->time();
            $ip = $this->request->ip();
            //验证是否注册过
            $mini_user = $MiniAppUser->where($where)->order('id desc')->find();
            if (!$mini_user || $mini_user['status'] == 1)
            {//如果用户没有注册 或者 注册用户已经注销了 ==> 注册账户
                //生成access_token和过期时间
                $avatarInfo = null;
                if ($avatar)
                {
                    $avatarInfo = Util::wxImgUploadOss($avatar, 'chakra_mini/uploads/avatar');
                    if (false === $avatarInfo['success'])
                    {
                        Log::write(self::LOG_PRE . '  上传三方头像失败，' . $avatarInfo['message'], 'error');
                    }
                }
                $avatar = $avatarInfo['success'] ? $avatarInfo['url'] : $avatar;

                $user['uen'] = substr(md5($this->request->time(true) . mt_rand(0, 1000000) . uniqid()), 0, 12);
                $user['nickname'] = $nickname;
                $user['avatar'] = $avatar;
                $user['reg_time'] = $time;
                $user['reg_ip'] = $ip;
                $user['last_login'] = $time;
                $user['last_ip'] = $ip;
                $user['last_active'] = $time;
                $user['status'] = 1;
                $user['sex'] = 1;
                $user['tel'] = ''; // 国内手机号不拼接国际手机区号
                $user['is_first_login'] = 1;
                $user['uc_id'] = md5($third_id . $this->request->time(true));
                $user['token'] = $tokenData['access_token'];
                $user['token_expire'] = $tokenData['expires_in'];
                $user['has_self_recode'] = 0;
                $user['channel'] = $this->headers['channel'] ?? 0;
                $user['app_type'] = $this->headers['app-type'] ?? '';
                try
                {
                    $user['id'] = $UserModel->insertGetId($user);
                }
                catch (\Exception $e)
                {
                    Log::write("脉轮 小程序注册失败，error:{$e->getMessage()};code: {$e->getCode()}; file: {$e->getFile()} ");
                    return show(config('code.error'), '注册失败,请稍后重试1');
                }
                $mini_user = [
                    'mini_type' => $miniType,
                    'nickname' => $nickname ?? '',
                    'avatar' => $avatar,
                    'sex' => $sex,
                    'unionid' => $unionid,
                    'openid' => $openid,
                    'city' => $city,
                    'province' => $province,
                    'country' => $country,
                    'create_time' => $time,
                    'app_type' => 1,
                    'status' => 0,
                    'uid' => $user['id'],
                    'channel' => $user['channel'], // 渠道ID
                ];
                $MiniAppUser->SavaData($mini_user);
            } else
            {   // 账户登陆
                $user = $UserModel->getInfoById($mini_user['uid']);
                // 更新数据库缓存信息
                $UserModel->updateUserinfo(['id' => $user['id']], [
                    'token' => $tokenData['access_token'],
                    'token_expire' => $tokenData['expires_in'],
                ]);
            }
            $id = (int)($user['id']);

            $data = [
                'access_token' => (new Aes())->encryptWithOpenssl($tokenData['access_token'] . "||" . $id, 'chakraapp'),
                'nickname' => $user['nickname'],
                'avatar' => $user['avatar'] ?: 'https://oss.goddessxzns.com/mini/chakra/imgs/avatar/default_avatar.png',
            ];
            // 记录登陆日志
            (new ChakraLoginLog())->insertGetId([
                'uid' => $id,
                'login_time' => $time,
                'login_type' => $loginType,
                'login_ip' => $ip,
            ]);
            if ($sessionKey)
            {
                $CacheSessionKey = $miniType . '_SESSION_KEY';
                Cache::set(constant('self::' . $CacheSessionKey), $sessionKey, 300);
            }
            return show(config('code.success'), '登录成功', $data);
        }
        catch (\Exception $e)
        {
            Log::write(self::LOG_PRE . '用户注册/登录失败，' . $e->getMessage() . '失败行数：' . $e->getLine(), 'error');
            throw new ApiException('登录失败', 200, 0);
        }
    }

    /**
     * 获取用户token信息
     * @param string $third_id
     * @return array
     */
    public function getUserTokenInfo($third_id = ''): array
    {
        // 生成token  和 token到期时间
        $token = IAuth::setAppLoginToken($third_id);
        $data = [
            'access_token' => $token,
            'expires_in' => strtotime('+' . config('chakraapp.login_time_out_day') . ' days'),
        ];
        return $data;
    }

    /**
     * 获取session key
     * @param $code
     * @param string $env_type SWAN：百度小程序；WEAPP：微信小程序；QQ:qq小程序；TT：字节跳动小程序；
     * @param array $mini_item_conf 单个小程序的配置
     * @return string|array
     */
    protected function getSessionKey($code, $env_type = '', $mini_item_conf = [])
    {
        try
        {
            switch ($env_type)
            {
                case 'SWAN':
                    $access_token_url = 'https://spapi.baidu.com/oauth/jscode2sessionkey?client_id=' . $mini_item_conf['app_key'] . '&sk=' . $mini_item_conf['app_secret'] . '&code=' . $code;
                    break;
                case 'WEAPP':
                    $access_token_url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' . $mini_item_conf['app_id'] . '&secret=' . $mini_item_conf['app_secret'] . '&js_code=' . $code . '&grant_type=authorization_code';
                    break;
                case 'QQ':
                    $access_token_url = 'https://api.q.qq.com/sns/jscode2session?appid=' . $mini_item_conf['app_id'] . '&secret=' . $mini_item_conf['app_secret'] . '&js_code=' . $code . '&grant_type=authorization_code';
                    break;
                case 'TT':
//                    $access_token_url = 'https://developer.toutiao.com/api/apps/v2/jscode2session?appid=' . $mini_item_conf['app_id'] . '&secret=' . $mini_item_conf['app_secret'] . '&code=' . $code;
                    $access_token_url = 'https://developer.toutiao.com/api/apps/v2/jscode2session';
//                    ?appid=' . $mini_item_conf['app_id'] . '&secret=' . $mini_item_conf['app_secret'] . '&code=' . $code;
                    break;
            }
            if (!empty($access_token_url))
            {
                if ($env_type === 'TT')
                {
                    $result = Curl::request($access_token_url, [
                        'appid' => $mini_item_conf['app_id'],
                        'secret' => $mini_item_conf['app_secret'],
                        'code' => $code,
                        'anonymous_code' => '',
                    ], 'POST', [], 'json');
                    $result_arr = $result['data']['data'];
                    return $result_arr;
                } else
                {
                    $result = file_get_contents($access_token_url);
                    $result_arr = json_decode($result, true);
                    if (isset($result_arr['session_key']))
                    {
                        return $result_arr['session_key'];
                    }
                }
            }
        }
        catch (Exception $e)
        {
            Log::write('获取 ' . $env_type . ' session key异常，信息为：' . $e->getMessage() . PHP_EOL, 'error', true);
        }

        return '';
    }

    /**
     * 今日头条小程序 验数据是否合法，并且解密数据 （解密参考： https://microapp.bytedance.com/dev/cn/mini-app/develop/open-capacity/user-information/sensitive-data-process）
     * @param $ciphertext
     * @param $iv
     * @param $session_key
     * @return bool|mixed
     * avatarUrl = "http://sf1-ttcdn-tos.pstatp.com/img/mosaic-legacy/3791/5070639578~120x256.image"
     * city = ""
     * country = "中国"
     * gender = {int} 0
     * language = ""
     * nickName = "zhunle"
     * openId = "2kfTH0Viw4tUSz6y"
     * province = ""
     * watermark = {array} [2]
     * appid = "tta48ad2373ba085a1"
     * timestamp = {int} 1575450447
     */
    protected function ttDecrypt($ciphertext, $iv, $session_key)
    {
        $session_key = base64_decode($session_key);
        $iv = base64_decode($iv);
        $ciphertext = base64_decode($ciphertext);

        if (function_exists('openssl_decrypt'))
        {
            $plaintext = openssl_decrypt($ciphertext, 'AES-128-CBC', $session_key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
        } else
        {
            $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, null, MCRYPT_MODE_CBC, null);
            mcrypt_generic_init($td, $session_key, $iv);
            $plaintext = mdecrypt_generic($td, $ciphertext);
            mcrypt_generic_deinit($td);
            mcrypt_module_close($td);
        }
        if ($plaintext === false)
        {
            return false;
        }

        // trim pkcs#7 padding
        $pad = ord(substr($plaintext, -1));
        $pad = ($pad < 1 || $pad > 32) ? 0 : $pad;
        $plaintext = substr($plaintext, 0, strlen($plaintext) - $pad);

        return json_decode(trim($plaintext), true);
    }

    /**
     * 微信或者QQ，检验数据的真实性，并且获取解密后的.
     * @param $appId
     * @param $sessionKey
     * @param $encryptedData string 加密的用户数据
     * @param $iv string 与用户数据一同返回的初始向量
     * @return array | int 成功返回详细信息，失败返回false，例如：avatarUrl = "https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epF3yvjxqJuRB9KydwTVHqEpia3wxwnBOkFUsDhMV9GwO9vcJ6tHlRbsEuicC9p7e859QCMVcRAt1uA/132"
     * city = "Chaoyang"
     * country = "China"
     * gender = {int} 1
     * language = "zh_CN"
     * nickName = "李柏楠"
     * openId = "oVpmX5KHiQXXabinAnBbYy3Um2ns"
     * province = "Beijing"
     * unionId = "oIVMdt71AOrtGBFSlYBe8zSDl5_o"
     * watermark = {array} [2]
     * appid = "wxa417c0a7a1e75643"
     * timestamp = {int} 1575437969
     */
    protected function wechatOrQQDecrypt($appId, $sessionKey, $encryptedData, $iv)
    {
        //qq登录没有unionid，有可能是我的测试号的原因，切换后再测试【解决：使用星座女神的qq号，登录qq开发平台进行的关联】
        if (strlen($sessionKey) !== 24)
        {
            return false;
        }
        $aesKey = base64_decode($sessionKey);
        if (strlen($iv) !== 24)
        {
            return false;
        }
        $aesIV = base64_decode($iv);
        $aesCipher = base64_decode($encryptedData);
        $result = openssl_decrypt($aesCipher, 'AES-128-CBC', $aesKey, 1, $aesIV);
        $data_arr = json_decode($result, true);
        if ($data_arr === NULL)
        {
            return false;
        }
        if ($data_arr['watermark']['appid'] !== $appId)
        {
            return false;
        }
        return $data_arr;
    }
}
